package com.droid.mvvmlib.databinding;

import com.droid.mvvmlib.log.Logcat;

import java.lang.reflect.InvocationTargetException;
import java.util.concurrent.atomic.AtomicBoolean;

import androidx.annotation.MainThread;
import androidx.annotation.NonNull;
import androidx.fragment.app.FragmentActivity;
import androidx.lifecycle.ViewModel;
import androidx.lifecycle.ViewModelProvider;
import androidx.lifecycle.ViewModelStore;
import androidx.lifecycle.ViewModelStoreOwner;

/**
 * support create parameterized provider
 */
public class ViewModelParameterizedProvider {
    private static final String TAG = "ViewModelParameterizedProvider";

    private AtomicBoolean mViewModeStoreSet = new AtomicBoolean(false);

    private ViewModelStore mViewModelStore = null;

    private Class<?>[] mConstructorParamTypes;

    static ViewModelParameterizedProvider getProvider() {
        return new ViewModelParameterizedProvider();
    }

    @MainThread
    public static ViewModelParameterizedProvider of(FragmentActivity activity) {
        return getProvider().ofActivity(activity);
    }

    @MainThread
    public static <T extends ViewModelStoreOwner> ViewModelParameterizedProvider of(T activity) {
        return getProvider().ofActivity(activity);
    }

    private <T extends ViewModelStoreOwner> ViewModelParameterizedProvider ofActivity(T activity) {
        checkForPreviousTargetsAndSet();
        mViewModelStore = activity.getViewModelStore();
        return this;
    }

    private ViewModelParameterizedProvider ofActivity(FragmentActivity activity) {
        checkForPreviousTargetsAndSet();
        mViewModelStore = activity.getViewModelStore();
        return this;
    }

    /**
     * constructor params type, If you do not specify a type, use the type of params
     */
    public ViewModelParameterizedProvider types(Class<?>... constructorParamsTypes) {
        mConstructorParamTypes = constructorParamsTypes;
        return this;
    }

    /**
     * constructor params
     * Don't use callbacks or Context parameters in order to avoid leaks!!
     */
    public ViewModelProvider with(Object... constructorParams) {
        ParametrizedFactory parametrizedFactory = new ParametrizedFactory(constructorParams);
        if (mConstructorParamTypes != null) {
            parametrizedFactory.setConstructorParamTypes(mConstructorParamTypes);
        }
        return new ViewModelProvider(mViewModelStore, parametrizedFactory);
    }

    private void checkForPreviousTargetsAndSet() {
        if (mViewModeStoreSet.get()) {
            Logcat.e(TAG, "ViewModelStore already has been set. Create new instance.");
        }
        mViewModeStoreSet.set(true);
    }

    private final static class ParametrizedFactory implements ViewModelProvider.Factory {
        private final Object[] mConstructorParams;
        private Class<?>[] mConstructorParamTypes;

        ParametrizedFactory(Object... constructorParams) {
            mConstructorParams = constructorParams;
        }

        void setConstructorParamTypes(Class<?>... constructorParamTypes) {
            mConstructorParamTypes = constructorParamTypes;
        }

        @Override
        @NonNull
        public <T extends ViewModel> T create(@NonNull Class<T> modelClass) {
            try {
                if (mConstructorParams == null || mConstructorParams.length == 0) {
                    Logcat.i(TAG, "Has created a new viewModel: " + modelClass.getSimpleName());
                    return modelClass.newInstance();
                } else {
                    Class<?>[] classes;
                    if (mConstructorParamTypes != null) {
                        classes = mConstructorParamTypes;
                    } else {
                        classes = new Class<?>[mConstructorParams.length];
                        for (int i = 0; i < mConstructorParams.length; i++) {
                            classes[i] = mConstructorParams[i].getClass();
                        }
                    }
                    Logcat.i(TAG, "Has created a new viewModelParameterized: " + modelClass.getSimpleName());
                    return modelClass.getConstructor(classes).newInstance(mConstructorParams);
                }
            } catch (InstantiationException e) {
                Logcat.e(TAG, "InstantiationException: " + e.toString());
            } catch (IllegalAccessException e) {
                Logcat.e(TAG, "IllegalAccessException: " + e.toString());
            } catch (NoSuchMethodException e) {
                Logcat.e(TAG, "NoSuchMethodException: " + e.toString());
            } catch (InvocationTargetException e) {
                Logcat.e(TAG, "InvocationTargetException: " + e.toString());
            }

            Logcat.e(TAG, "create view model class error: " + modelClass.getSimpleName());
            //noinspection ConstantConditions
            return null;
        }
    }
}